package model;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import solver.BaseExtraLeanIPModelBuilder;

import etm.core.configuration.EtmManager;
import etm.core.monitor.EtmMonitor;
import etm.core.monitor.EtmPoint;

public class EnhancedCostEvaluator {
	// private static final EtmMonitor etmMonitor = EtmManager.getEtmMonitor();
	private static Logger log = LoggerFactory
			.getLogger(EnhancedCostEvaluator.class);

	SolverEnvironment env;

	long initial_load_cost;
	long initial_balance_cost;

	long new_load_cost;
	long new_balance_cost;
	long process_move_cost;
	long machine_move_cost;
	long service_move_cost;

	public EnhancedCostEvaluator() {
	}

	public EnhancedCostEvaluator(SolverEnvironment env) {
		this.env = env;
	}

	/**
	 * The potential solution should be set through the environment.
	 * 
	 * @param new_solution
	 */
	@Deprecated
	public void setPotentialSolution(Solution new_solution) {
		env.setPotentialSolution(new_solution);
	}

	/**
	 * The initial_solution and potential solution should be set through the
	 * environment.
	 * 
	 * @param initial_solution
	 * @param new_solution
	 */
	@Deprecated
	public void setSolutionPair(Solution initial_solution, Solution new_solution) {
		// env.setInitialSolution(initial_solution);
		env.setPotentialSolution(new_solution);
	}

	public long getInitialSolutionLoadCost() {
		return initial_load_cost;
	}

	public long getInitialSolutionBalanceCost() {
		return initial_balance_cost;
	}

	public long getNewSolutionLoadCost() {
		return new_load_cost;
	}

	public long getNewSolutionBalanceCost() {
		return new_balance_cost;
	}

	public long getProcessMoveCost() {
		return process_move_cost;
	}

	public long getMachineMoveCost() {
		return machine_move_cost;
	}

	public long getServiceMoveCost() {
		return service_move_cost;
	}

	// before each call it should be ensured that the correct pair of solutions
	// are loaded. Setting the pair of solutions occurs via
	// setSolutionPair(initial_solution, new_solution)
	public boolean isFeasible() {
		// EtmPoint point = etmMonitor
		// .createPoint("EnhancedCostEvaluator:isFeasible");
		boolean flag = true;
		if (!isFeasibleForCapacityConstraints())
			flag = false;
		if (!isFeasibleForConflictConstraints())
			flag = false;
		if (!isFeasibleForSpreadConstraints())
			flag = false;
		if (!isFeasibleForDependencyConstraints())
			flag = false;
		if (!isFeasibleForTransientConstraints())
			flag = false;
		// point.collect();
		return flag;
	}

	public boolean isFeasibleForCapacityConstraints() {
		boolean flag = true;
		for (int i = 0; i < getProblem().M; i++)
			for (int j = 0; j < getProblem().RE; j++)
				if (getNewSolution().usage[i][j] > getProblem().C[i][j]) {
					if (getProblem().VERBOSE) {
						System.out
								.printf("Not feasible [CAPACITY VIOLATION] Machine:%d resource:%d (needed:%d available:%d)\n",
										i, j, getNewSolution().usage[i][j],
										getProblem().C[i][j]);
					}
					flag = false;
				}
		return flag;
	}

	public boolean isFeasibleForConflictConstraints() {
		boolean flag = true;
		for (int s_index = 0; s_index < getProblem().S; s_index++) {
			Set<Integer> aSet = new HashSet<Integer>();
			for (Integer p_index : getProblem().processesListForServices[s_index]) {
				aSet.add(getNewSolution().processes[p_index]);
			}
			if (aSet.size() < getProblem().processesListForServices[s_index]
					.size()) {
				if (getProblem().VERBOSE) {
					Service aService = getProblem().getServices().get(s_index);
					System.out
							.printf("Not feasible [CONFLICT VIOLATION] Processes %s of the same service (s%d) should not be scheduled in the same machine\n",
									aService.getProcesses(), s_index);
					for (Integer p_id : aService.getProcesses()) {
						System.out.printf("p%d->m%d\n", p_id,
								getNewSolution().processes[p_id]);
					}
				}
				flag = false;
			}
		}
		return flag;
	}

	public boolean isFeasibleForSpreadConstraints() {
		for (int s_index = 0; s_index < getProblem().S; s_index++) {
			int spreadMin = getProblem().services.get(s_index).spreadMin;
			Set<Integer> aSet = new HashSet<Integer>();
			for (Integer p_index : getProblem().processesListForServices[s_index]) {
				int m_index = getNewSolution().processes[p_index];
				aSet.add(getProblem().machines_locations[m_index]);
			}
			if (aSet.size() < spreadMin) {
				if (getProblem().VERBOSE)
					System.out
							.printf("Not feasible [SPREAD CONSTRAINT VIOLATION] s%d should have spreadMin=%d but is %d %s\n",
									s_index, spreadMin, aSet.size(), aSet);
				return false;
			}
		}
		return true;
	}

	public boolean isFeasibleForDependencyConstraints() {
		boolean flag = true;
		for (int s_indexa = 0; s_indexa < getProblem().S; s_indexa++) {
			if (getProblem().service_dependencies[s_indexa].isEmpty())
				continue;
			else {
				// System.out.printf("Service %d depends on %s\n", s_indexa,
				// problem.service_dependencies[s_indexa]);
			}
			for (Integer service_dep : getProblem().service_dependencies[s_indexa]) {
				Set<Integer> neighborhoods = new HashSet<Integer>();
				// System.out.printf("Service %d processes %s\n", service_dep,
				// problem.processesListForServices[service_dep]);
				for (Integer p_index : getProblem().processesListForServices[service_dep]) {
					int m_index = getNewSolution().processes[p_index];
					int ni_index = getProblem().machines_neighborhoods[m_index];
					// System.out.printf(
					// "Process %d Machine %d neighborhood %d\n", p_index,
					// m_index, ni_index);
					neighborhoods.add(ni_index);
				}
				// System.out.println("Neighborhoods " + neighborhoods);
				for (Integer p_index : getProblem().processesListForServices[s_indexa]) {
					int m_index = getNewSolution().processes[p_index];
					int n_i = getProblem().machines_neighborhoods[m_index];
					if (!neighborhoods.contains(new Integer(n_i))) {
						// System.out.printf("Neighborhoods %s (%d - %d) \n",
						// neighborhoods, m_index, n_i);
						if (getProblem().VERBOSE)
							System.out
									.printf("Not feasible [DEPENDENCY CONSTRAINT VIOLATION] Process %d of service %d should run in the neighborhood of a process of service %d runs.\n",
											p_index, s_indexa, service_dep);
						flag = false;
					}
				}
			}
		}
		return flag;
	}

	public boolean isFeasibleForTransientConstraints() {
		boolean flag = true;
		for (int m = 0; m < getProblem().M; m++)
			for (int r = 0; r < getProblem().RE; r++) {
				Resource resource = getProblem().getResourceByIndex(r);
				if (resource.isTransientUsage()) {
					long sum = 0;
					for (int p = 0; p < getProblem().P; p++) {
						int initial_state_machine = getInitialSolution().processes[p];
						int new_state_machine = getNewSolution().processes[p];
						if ((initial_state_machine == m)
								|| (new_state_machine == m)) {
							sum += getProblem().getR(p, r);
						}
					}
					if (sum > getProblem().getC(m, r)) {
						if (getProblem().VERBOSE)
							System.out
									.printf("Not feasible [TRANSIENT CONSTRAINT VIOLATION]. Machine %d and resource %d capacity problem\n",
											m, r);
						flag = false;
					}
				}
			}
		return flag;
	}

	private long computeLoadCost(Solution aSolution) {
		// EtmPoint point = etmMonitor
		// .createPoint("EnhancedCostEvaluator:getLoadCost");
		long sum = 0;
		for (int r = 0; r < getProblem().RE; r++) {
			long sum2 = 0;
			for (int m = 0; m < getProblem().M; m++) {
				int usage = aSolution.getUsageOfMachineForResource(m, r);
				int safety_capacity = getProblem().SC[m][r];
				int delta = usage - safety_capacity;
				int max;
				if (delta > 0)
					max = delta;
				else
					max = 0;
				// is_load_cost_MxR[m][r] = max;
				sum2 += max
						* getProblem().getResourceByIndex(r)
								.getWeightLoadCost();
				// System.out.printf("[ECE] load cost m%dr%d = %d\n", m, r,
				// max);
			}
			sum += sum2;
		}
		// System.out.println();
		// point.collect();
		return sum;
	}

	public long computeLoadCostPerMachine(Solution aSolution, int m) {
		long sum = 0;
		for (int r = 0; r < getProblem().RE; r++) {
			Resource aResource = getProblem().getResourceByIndex(r);
			sum += computeUnweightedLoadCostPerMachineAndResource(aSolution, m,
					r) * aResource.getWeightLoadCost();
		}
		return sum;
	}

	public long computeUnweightedLoadCostPerMachineAndResource(
			Solution aSolution, int m, int r) {
		int usage = aSolution.getUsageOfMachineForResource(m, r);
		int safety_capacity = getProblem().SC[m][r];
		int delta = usage - safety_capacity;
		int max;
		if (delta > 0)
			max = delta;
		else
			max = 0;
		// System.out.printf("[ECE] load cost(unweighted) m%d,r%d = %,d\n", m,
		// r,
		// max);
		return max;
	}

	private long computeInitialSolutionLoadCost() {
		return computeLoadCost(getInitialSolution());
	}

	private long computeNewSolutionLoadCost() {
		return computeLoadCost(getNewSolution());
	}

	private long computeBalanceCost(Solution aSolution) {
		// EtmPoint point = etmMonitor
		// .createPoint("EnhancedCostEvaluator:getBalanceCost");
		long sum = 0;
		// int bo_index = 0;
		for (BalanceObjective bo : getProblem().getBalanceObjectives()) {
			for (int m = 0; m < getProblem().M; m++) {
				int amr1 = getProblem().C[m][bo.resourcea]
						- aSolution.getUsageOfMachineForResource(m,
								bo.resourcea);
				int amr2 = getProblem().C[m][bo.resourceb]
						- aSolution.getUsageOfMachineForResource(m,
								bo.resourceb);
				int delta = bo.target * amr1 - amr2;
				int max;
				if (delta > 0) {
					max = delta;
				} else {
					max = 0;
				}
				// System.out.printf("[ECE] balance cost bo%d_m%d = %d\n",
				// bo_index, m,
				// max);
				sum += max * bo.weightBalanceCost;
			}
			// System.out.println();
			// bo_index++;
		}
		// point.collect();
		return sum;
	}

	// not checked (13/2/2012)
	private long computeBalanceCostPerMachine(Solution aSolution, int m) {
		long sum = 0;
		for (BalanceObjective bo : getProblem().getBalanceObjectives()) {
			int amr1 = getProblem().C[m][bo.resourcea]
					- aSolution.getUsageOfMachineForResource(m, bo.resourcea);
			int amr2 = getProblem().C[m][bo.resourceb]
					- aSolution.getUsageOfMachineForResource(m, bo.resourceb);
			int delta = bo.target * amr1 - amr2;
			int max;
			if (delta > 0) {
				max = delta;
			} else {
				max = 0;
			}
			// System.out.printf("[ECE] balance cost bo%d_m%d = %d\n",
			// bo_index, m,
			// max);
			sum += max * bo.weightBalanceCost;
		}
		return sum;
	}

	public long computeInitialSolutionBalanceCost() {
		return computeBalanceCost(getInitialSolution());
	}

	public long computeNewSolutionBalanceCost() {
		return computeBalanceCost(getNewSolution());
	}

	private long computeProcessMoveCost() {
		// EtmPoint point = etmMonitor
		// .createPoint("EnhancedCostEvaluator:getProcessMoveCost");
		long sum = 0;
		for (int p = 0; p < getProblem().P; p++) {
			if (getInitialSolution().getMachineIdForProcessId(p).intValue() != getNewSolution()
					.getMachineIdForProcessId(p).intValue()) {
				Process process = getProblem().getProcesses().get(p);
				sum += process.getProcessMoveCost();
			}
		}
		// point.collect();
		return sum * getProblem().getWeightProcessMoveCost();
	}

	private long computeServiceMoveCost() {
		// EtmPoint point = etmMonitor
		// .createPoint("EnhancedCostEvaluator:getServiceMoveCost");
		long max = 0;
		for (int s = 0; s < getProblem().S; s++) {
			Service aService = getProblem().getServices().get(s);
			long c = 0;
			for (Integer p : aService.getProcesses()) {
				if (getInitialSolution().getMachineIdForProcessId(p).intValue() != getNewSolution()
						.getMachineIdForProcessId(p).intValue()) {
					c++;
				}
			}
			if (c > max)
				max = c;
		}
		// point.collect();
		return max * getProblem().getWeightServiceMoveCost();
	}

	private long computeMachineMoveCost() {
		// EtmPoint point = etmMonitor
		// .createPoint("EnhancedCostEvaluator:getMachineMoveCost");
		long sum = 0;
		for (int p = 0; p < getProblem().P; p++) {
			int initial_machine_id = getInitialSolution()
					.getMachineIdForProcessId(p);
			Machine aMachine = getProblem().getMachines().get(
					initial_machine_id);
			int new_machine_id = getNewSolution().getMachineIdForProcessId(p);
			if (new_machine_id != -1)
				sum += aMachine.getMachineMoveCost(new_machine_id);
		}
		// point.collect();
		return sum * getProblem().getWeightMachineMoveCost();
	}

	// before each call
	public void computeCost() {
		// EtmPoint point = etmMonitor.createPoint("EnhancedCostEvaluator");
		initial_load_cost = computeInitialSolutionLoadCost();
		initial_balance_cost = computeInitialSolutionBalanceCost();
		new_load_cost = computeNewSolutionLoadCost();
		new_balance_cost = computeNewSolutionBalanceCost();
		machine_move_cost = computeMachineMoveCost();
		service_move_cost = computeServiceMoveCost();
		process_move_cost = computeProcessMoveCost();
		// point.collect();
	}

	public long getInitialSolutionCost() {
		return initial_load_cost + initial_balance_cost;
	}

	public long getNewSolutionCost() {
		return new_load_cost + new_balance_cost + process_move_cost
				+ service_move_cost + machine_move_cost;
	}

	public void printCostAnalysis() {
		printInitialSolutionCostAnalysis();
		printNewSolutionCostAnalysis("New Solution");
	}

	public void printInitialSolutionCostAnalysis() {
		StringBuilder sb = new StringBuilder();
		sb.append(String.format(
				"[%sSolution=%,d] load cost=%,d balance cost=%,d",
				(getNewSolution() == null) ? "" : "INI ",
				getInitialSolutionCost(), initial_load_cost,
				initial_balance_cost));
		// sb.append("\n");
		log.debug(sb.toString());
	}

	public void printNewSolutionCostAnalysis(String s) {
		StringBuilder sb = new StringBuilder();
		sb.append(String
				.format("[%s=%,d] load cost=%,d balance cost=%,d process move cost=%,d service move cost=%,d machine move cost=%,d",
						s, getNewSolutionCost(), new_load_cost,
						new_balance_cost, process_move_cost, service_move_cost,
						machine_move_cost));
		// sb.append("\n");
		log.debug(sb.toString());
	}

	public long[] getCostVector() {
		return new long[] { new_load_cost, new_balance_cost, process_move_cost,
				service_move_cost, machine_move_cost };
	}

	public void printMoves(int cols) {
		StringBuilder sb = new StringBuilder();
		int c = 0;
		for (int p_id = 0; p_id < getProblem().P; p_id++) {
			int m_id1 = getNewSolution().getMachineIdForProcessId(p_id);
			int m_id2 = getInitialSolution().getMachineIdForProcessId(p_id);
			if (m_id1 != m_id2) {
				sb.append(String.format("p%d m%d->m%d\t", p_id, m_id1, m_id2));
				c++;
				if (c % cols == 0)
					sb.append("\n");
			}
		}
		System.out.println(sb.toString());
	}

	private Problem getProblem() {
		return env.getProblem();
	}

	private Solution getInitialSolution() {
		return env.getInitialSolution();
	}

	public Solution getNewSolution() {
		return env.getPotentialSolution();
	}

	public SolverEnvironment getEnv() {
		return env;
	}

	public void init() {
		updateStructuresOnInitialSolutionChange();
	}

	public void setEnv(SolverEnvironment env) {
		this.env = env;
		init();
	}

	public void updateStructuresOnInitialSolutionChange() {
		initial_load_cost = getInitialSolutionLoadCost();
		initial_balance_cost = getInitialSolutionBalanceCost();
	}

	public void updateStructuresOnSolutionChange() {
		computeCost();
	}

	public List<Integer> getUnscheduledProcesses() {
		List<Integer> unscheduled = new ArrayList<Integer>();
		for (int i = 0; i < env.getProblem().P; i++) {
			if (env.getCurrentSolution().getMachineIdForProcessId(i) == -1)
				unscheduled.add(i);
		}
		return unscheduled;
	}

	// ########## SHOULD NOT BE USED ##########
	@Deprecated
	public long computeAndGetCutOff(List<Integer> selectedProcesses) {
		Set<Integer> selectedMachines = new HashSet<Integer>();
		for (Integer p : selectedProcesses) {
			selectedMachines.addAll(env.getProblem().getEligibleMachinesFor(p));
		}
		List<Integer> sortedSelectedMachines = new ArrayList<Integer>(
				selectedMachines);
		Collections.sort(sortedSelectedMachines);
		long sum_lc = 0;
		for (Integer sm : sortedSelectedMachines) {
			sum_lc += computeLoadCostPerMachine(env.getCurrentSolution(), sm);
		}

		long sum_bc = 0;
		for (Integer sm : sortedSelectedMachines) {
			sum_lc += computeBalanceCostPerMachine(env.getCurrentSolution(), sm);
		}

		long pmc = computeProcessMoveCost();
		long smc = computeServiceMoveCost();
		long mmc = computeMachineMoveCost();

		return sum_lc + sum_bc + pmc + smc + mmc;
	}

}
